﻿using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;

using AMC.SubstitutionCache.Rendering;
using AMC.SubstitutionCache.Services;
using JetBrains.Annotations;
using Orchard.ContentManagement;
using Orchard.ContentManagement.Drivers;
using Orchard.ContentManagement.Handlers;
using Orchard.ContentManagement.MetaData;
using Orchard.DisplayManagement;
using Orchard.DisplayManagement.Descriptors;
using Orchard.DisplayManagement.Shapes;

namespace AMC.SubstitutionCache.Drivers
{
    public abstract class SubstitutionDriver<TContent> : SubstitutionDriverBase, ISubstitutionDriver
        where TContent : ContentPart, new()
    {
        private readonly IList<Func<SubstitutionDriverRenderingInfo>> _handledShapes;

        protected SubstitutionDriver()
        {
            _handledShapes = new List<Func<SubstitutionDriverRenderingInfo>>();
        }

        #region ISubstitutionDriver Implementation

        public IEnumerable<SubstitutionDriverRenderingInfo> HandledShapes
        {
            get
            {
                return _handledShapes.Select(f => f());
            }
        }

        protected SubstitutionDriverRenderingInfo CreateHandledShape(string shapeType)
        {
            return CreateHandledShape(shapeType, string.Empty, false, null);
        }

        protected SubstitutionDriverRenderingInfo CreateHandledShape(string shapeType, string groupId)
        {
            return CreateHandledShape(shapeType, groupId, false, null);
        }

        protected SubstitutionDriverRenderingInfo CreateHandledShape(string shapeType, string groupId, bool useEditor)
        {
            return CreateHandledShape(shapeType, groupId, useEditor, null);
        }

        protected SubstitutionDriverRenderingInfo CreateHandledShape(string shapeType, string groupId, bool useEditor, Func<dynamic, SubstitutionContext, bool> shapeLocator)
        {
           return new SubstitutionDriverRenderingInfo(typeof(TContent), shapeType, this, groupId, useEditor, shapeLocator);
        }

        protected void AddHandledShape(Func<SubstitutionDriverRenderingInfo> info)
        {
            _handledShapes.Add(info);
        }
        

        [NotNull]
        public virtual SubstitutionCacheInfo GetCacheInfo(SubstitutionContext context)
        {
            var info = new SubstitutionCacheInfo();

            if (context.ContentId > 0)
                info.Tags.Add(ShapeHelper.GetPartTag<TContent>(context.ContentId));

            return info;
        }

        #endregion

        protected virtual Action<BuildShapeContext, dynamic> ShapeConfig { get { return (context, shape) => { }; } }

        protected virtual string Prefix { get { return typeof(TContent).Name; } }

        protected virtual string GroupId { get { return string.Empty; } }

        protected virtual bool IgnoreGroupId { get { return false; } }

        #region IContentPartDriver Implementation

        void IContentPartDriver.GetContentItemMetadata(GetContentItemMetadataContext context)
        {
            var part = context.ContentItem.As<TContent>();
            if (part != null)
                GetContentItemMetadata(part, context.Metadata);
        }

        DriverResult IContentPartDriver.BuildDisplay(BuildDisplayContext context)
        {
            if (!IgnoreGroupId && !string.Equals(GroupId, context.GroupId))
                return null;

            var part = context.ContentItem.As<TContent>();

            if (part == null)
                return null;

            DriverResult result = Display(part, context.DisplayType, context.New);

            if (result != null)
                result.ContentPart = part;

            return result;
        }

        DriverResult IContentPartDriver.BuildEditor(BuildEditorContext context)
        {
            if (!IgnoreGroupId && !string.Equals(GroupId, context.GroupId))
                return null;

            var part = context.ContentItem.As<TContent>();

            if (part == null)
            {
                return null;
            }

            DriverResult result = Editor(part, context.New);

            if (result != null)
            {
                result.ContentPart = part;
            }

            return result;
        }

        DriverResult IContentPartDriver.UpdateEditor(UpdateEditorContext context)
        {
            if (!IgnoreGroupId && !string.Equals(GroupId, context.GroupId))
                return null;

            var part = context.ContentItem.As<TContent>();

            if (part == null)
            {
                return null;
            }

            // checking if the editor needs to be updated (e.g. if it was not hidden)
            var editor = Editor(part, context.New) as ContentShapeResult;

            if (editor != null)
            {
                ShapeDescriptor descriptor;
                if (context.ShapeTable.Descriptors.TryGetValue(editor.GetShapeType(), out descriptor))
                {
                    var placementContext = new ShapePlacementContext
                    {
					    Content = part.ContentItem,
                        ContentType = part.ContentItem.ContentType,
                        Differentiator = editor.GetDifferentiator(),
                        DisplayType = null,
                        Path = string.Empty
                    };

                    var location = descriptor.Placement(placementContext).Location;

                    if (string.IsNullOrEmpty(location) || location == "-")
                    {
                        return editor;
                    }
                }
            }

            DriverResult result = Editor(part, context.Updater, context.New);

            if (result != null)
                result.ContentPart = part;

            return result;
        }

        void IContentPartDriver.Importing(ImportContentContext context)
        {
            var part = context.ContentItem.As<TContent>();
            if (part != null)
                Importing(part, context);
        }

        void IContentPartDriver.Imported(ImportContentContext context)
        {
            var part = context.ContentItem.As<TContent>();
            if (part != null)
                Imported(part, context);
        }

        void IContentPartDriver.Exporting(ExportContentContext context)
        {
            var part = context.ContentItem.As<TContent>();
            if (part != null)
                Exporting(part, context);
        }

        void IContentPartDriver.Exported(ExportContentContext context)
        {
            var part = context.ContentItem.As<TContent>();
            if (part != null)
                Exported(part, context);
        }

        public IEnumerable<ContentPartInfo> GetPartInfo()
        {
            var contentPartInfo = new[] 
            {
                new ContentPartInfo 
                {
                    PartName = typeof(TContent).Name,
                    Factory = typePartDefinition => new TContent { TypePartDefinition = typePartDefinition }
                }
            };

            return contentPartInfo;
        }

        #endregion

        #region Protected Default Implementations

        protected virtual void GetContentItemMetadata(TContent context, ContentItemMetadata metadata) { }

        protected virtual DriverResult Display(TContent part, string displayType, dynamic shapeHelper) { return null; }
        protected virtual DriverResult Editor(TContent part, dynamic shapeHelper) { return null; }
        protected virtual DriverResult Editor(TContent part, IUpdateModel updater, dynamic shapeHelper) { return null; }

        protected virtual void Importing(TContent part, ImportContentContext context) { }
        protected virtual void Imported(TContent part, ImportContentContext context) { }
        protected virtual void Exporting(TContent part, ExportContentContext context) { }
        protected virtual void Exported(TContent part, ExportContentContext context) { }

        #endregion

        public ContentShapeResult ContentShape(string shapeType, Func<dynamic> factory)
        {
            return ContentShapeImplementation(shapeType, ctx => factory());
        }

        public ContentShapeResult ContentShape(string shapeType, Func<dynamic, dynamic> factory)
        {
            return ContentShapeImplementation(shapeType, ctx => factory(CreateShape(ctx, shapeType)));
        }
		
		private ContentShapeResult ContentShapeImplementation(string shapeType, Func<BuildShapeContext, object> shapeBuilder)
        {
            if (IgnoreGroupId)
                return ContentShapeIgnoreGroupImplementation(shapeType, shapeBuilder);

            return new ContentShapeResult(shapeType, Prefix, ctx =>
            {
                var shape = shapeBuilder(ctx);

                if (shape == null)
                    return null;

                if (!string.IsNullOrWhiteSpace(ctx.GroupId))
                    (shape as IShape).SetGroupId(ctx.GroupId);

                if (ShapeConfig != null)
                    ShapeConfig(ctx, shape);

                return AddAlternates(shape, ctx);
            });
        }

        public ContentShapeResult ContentShapeIgnoreGroup(string shapeType, Func<dynamic> factory)
        {
            return ContentShapeIgnoreGroupImplementation(shapeType, ctx => factory());
        }

        public ContentShapeResult ContentShapeIgnoreGroup(string shapeType, Func<dynamic, dynamic> factory)
        {
            return ContentShapeIgnoreGroupImplementation(shapeType, ctx => factory(CreateShape(ctx, shapeType)));
        }
		
		private ContentShapeResult ContentShapeIgnoreGroupImplementation(string shapeType, Func<BuildShapeContext, object> shapeBuilder)
        {
            var result = new ContentShapeIgnoreGroupResult(shapeType, Prefix, ctx =>
            {
                var shape = shapeBuilder(ctx);

                if (shape == null)
                    return null;

                if (ShapeConfig != null)
                    ShapeConfig(ctx, shape);

                return AddAlternates(shape, ctx);
            });

            return result;
        }
		
        protected virtual ContentShapeResult PlaceholderShape(TContent part, dynamic shapeHelper, string shapeType, string displayType)
        {
            return PlaceholderShape(part, shapeHelper, shapeType, shapeType, string.Empty, displayType);
        }

        protected ContentShapeResult PlaceholderShape(TContent part, dynamic shapeHelper, string shapeTypeName, string contextShapeTypeName, string templateName, string displayType)
        {
            return ContentShapeImplementation(shapeTypeName, ctx =>
                {
                    var shape = (IShape)shapeHelper.SubstitutionPlaceholder();
                    var token = new SubstitutionContext(contextShapeTypeName, templateName, part, displayType, IgnoreGroupId ? string.Empty : GroupId);
                    shape.SetSubstitutionContext(token);
                    return shape;
                });
        }

        private static dynamic AddAlternates(dynamic shape, BuildShapeContext ctx)
        {
            ShapeMetadata metadata = shape.Metadata;

            // if no ContentItem property has been set, assign it
            if (shape.ContentItem == null)
            {
                shape.ContentItem = ctx.ContentItem;
            }

            var shapeType = metadata.Type;

            // [ShapeType]__[Id] e.g. Parts/Common.Metadata-42
            metadata.Alternates.Add(shapeType + "__" + ctx.ContentItem.Id.ToString(CultureInfo.InvariantCulture));

            // [ShapeType]__[ContentType] e.g. Parts/Common.Metadata-BlogPost
            metadata.Alternates.Add(shapeType + "__" + ctx.ContentItem.ContentType);

            return shape;
        }

        private static object CreateShape(BuildShapeContext context, string shapeType)
        {
            IShapeFactory shapeFactory = context.New;
            return shapeFactory.Create(shapeType);
        }

     
        public CombinedResult Combined(params DriverResult[] results)
        {
            return new CombinedResult(results);
        }
    }
}